複数ファイルに分割されたスライド PDF を、1つに結合して、しおりを追加する Python プラグラムを作成してみた

複数ファイルに分割されたスライド PDF を、1つに結合して、しおりを追加する Python プラグラムを作成してみた

Clock Icon2024.10.31

はじめに

アノテーション テクニカルサポートチームの 川崎 です。

Udemy のコースを受講する際、スライドのPDFファイルが提供されているコースがあります。

1ファイルにまとまっていると参照しやすいのですが、章ごとに分割されているものがありました。
分割された PDF ファイルを使ってみると見づらいな、と感じました。

そこで、参照しやすいように、スライドの PDF ファイルを1ファイルに結合する Python プログラムを作成しました。備忘録としてブログにします。

この記事では、具体的なコード例を示しながら、各ステップを説明します。

1. プログラムの概要

このプログラムは、以下の機能を提供します:

  • 複数のPDFファイルを一つにマージ
  • 各ページにヘッダーを追加
  • 各PDFの最初の見出しを抽出し、それをブックマークとして設定

2. 必要なライブラリのインストール

このプログラムを実行するためには、以下のPythonライブラリが必要です:

  • PyPDF2
  • reportlab
  • tqdm

これらは以下のコマンドでインストールできます:

pip install PyPDF2 reportlab tqdm

3. ログ設定

プログラムの冒頭で、ログの設定を行っています。ログはプログラムの実行状況やエラーを記録するために重要です。

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

4. 日本語フォントの登録

日本語フォントを使用するために、reportlabライブラリを使ってフォントを登録します。フォントファイルのパスは適切に変更してください。

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

FONT_PATH = '/path/to/your/font/HiraKakuProN-W3-AlphaNum-01.ttf'
FONT_NAME = 'HiraKakuProN'
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))

5. PDFから最初の見出しを抽出する関数

この関数は、PDFの各ページからテキストを抽出し、最初の見出しを見つけて返します。

def extract_first_heading(pdf_reader):
    for page in pdf_reader.pages:
        try:
            text = page.extract_text()
            lines = text.split('\\n')
            for line in lines:
                line = line.strip()
                if line and len(line) < 100:  # 見出しは通常短いため
                    return line
        except Exception as e:
            logger.warning(f"Error extracting text from page: {str(e)}")
    return None

6. ファイル名から数字を抽出する関数

ファイル名から数字を抽出し、ブックマークのタイトルに使用します。

def extract_number_from_filename(filename):
    match = re.search(r'\\d+', filename)
    return match.group() if match else ''

7. PDFページにヘッダーを追加する関数

この関数は、PDFページに指定されたテキストをヘッダーとして追加します。

def add_header(page, text):
    packet = io.BytesIO()
    width = float(page.mediabox.width)
    height = float(page.mediabox.height)
    can = canvas.Canvas(packet, pagesize=(width, height))
    can.setFont(FONT_NAME, 10)
    text_width = can.stringWidth(text, FONT_NAME, 10)
    can.drawString(width - text_width - 5, height - 15, text)
    can.save()
    packet.seek(0)
    new_pdf = PdfReader(packet)
    page.merge_page(new_pdf.pages[0])
    return page

8. 複数のPDFをマージし、ブックマークとヘッダーを追加する関数

この関数は、複数のPDFファイルをマージし、各ページにヘッダーを追加し、ブックマークを設定します。

def merge_pdfs_with_bookmarks_and_header(pdf_files, output_pdf):
    merger = PdfMerger()
    writer = PdfWriter()
    total_pages = 0
    bookmarks = []

    logger.info("Merging PDFs and extracting first headings")
    for pdf_file in tqdm(pdf_files, desc="Processing PDFs"):
        if not os.path.exists(pdf_file):
            logger.warning(f"File not found: {pdf_file}")
            continue

        try:
            pdf_reader = PdfReader(pdf_file)

            first_heading = extract_first_heading(pdf_reader)
            if not first_heading:
                first_heading = os.path.splitext(os.path.basename(pdf_file))[0]

            file_number = extract_number_from_filename(pdf_file)
            bookmark_title = f"{file_number}. {first_heading}" if file_number else first_heading

            bookmarks.append((bookmark_title, total_pages))

            for page in pdf_reader.pages:
                new_page = add_header(page, bookmark_title)
                writer.add_page(new_page)

            total_pages += len(pdf_reader.pages)
        except Exception as e:
            logger.error(f"Error processing {pdf_file}: {str(e)}")

    for title, page_num in bookmarks:
        writer.add_outline_item(title, page_num)

    logger.info("Saving final PDF")
    with open(output_pdf, 'wb') as f:
        writer.write(f)

    logger.info(f"Merged PDF with bookmarks and headers saved as {output_pdf}")

9. 使用例

最後に、プログラムの使用例を示します。pdf_filesリストにマージしたいPDFファイルのパスを指定し、output_pdfに出力ファイルのパスを指定します。

pdf_files = [
    "slide_01.pdf",
    "slide_02.pdf",
    "slide_03.pdf",
    "slide_04.pdf",
    "slide_05.pdf"
]
output_pdf = "merged_with_bookmarks_and_headers.pdf"
merge_pdfs_with_bookmarks_and_header(pdf_files, output_pdf)

10. ソースコード全体

import os
import logging
import re
import io
from PyPDF2 import PdfMerger, PdfReader, PdfWriter
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from tqdm import tqdm

# ログ設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 日本語フォントの登録(パスは適切に変更してください)
FONT_PATH = '/Users/kawasaki.teruo/20241012/PDF/file/HiraKakuProN-W3-AlphaNum-01.ttf'
FONT_NAME = 'HiraKakuProN'
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))

def extract_first_heading(pdf_reader):
    """
    PDFから最初の見出しを抽出する関数
    """
    for page in pdf_reader.pages:
        try:
            text = page.extract_text()
            lines = text.split('\n')
            for line in lines:
                line = line.strip()
                if line and len(line) < 100:  # 見出しは通常短いため
                    return line
        except Exception as e:
            logger.warning(f"Error extracting text from page: {str(e)}")
    return None

def extract_number_from_filename(filename):
    """
    ファイル名から数字を抽出する関数
    """
    match = re.search(r'\d+', filename)
    return match.group() if match else ''

def add_header(page, text):
    """
    PDFページにヘッダーを追加する関数
    """
    packet = io.BytesIO()
    width = float(page.mediabox.width)
    height = float(page.mediabox.height)
    can = canvas.Canvas(packet, pagesize=(width, height))
    can.setFont(FONT_NAME, 10)
    text_width = can.stringWidth(text, FONT_NAME, 10)
    can.drawString(width - text_width - 5, height - 15, text)
    can.save()
    packet.seek(0)
    new_pdf = PdfReader(packet)
    page.merge_page(new_pdf.pages[0])
    return page

def merge_pdfs_with_bookmarks_and_header(pdf_files, output_pdf):
    """
    複数のPDFをマージし、ブックマークとヘッダーを追加する関数
    """
    merger = PdfMerger()
    writer = PdfWriter()
    total_pages = 0
    bookmarks = []

    logger.info("Merging PDFs and extracting first headings")
    for pdf_file in tqdm(pdf_files, desc="Processing PDFs"):
        if not os.path.exists(pdf_file):
            logger.warning(f"File not found: {pdf_file}")
            continue

        try:
            pdf_reader = PdfReader(pdf_file)

            # 最初の見出しを抽出
            first_heading = extract_first_heading(pdf_reader)

            # 見出しが見つからない場合はファイル名を使用
            if not first_heading:
                first_heading = os.path.splitext(os.path.basename(pdf_file))[0]

            # ファイル名から番号を抽出
            file_number = extract_number_from_filename(pdf_file)

            # 番号と見出しを結合
            bookmark_title = f"{file_number}. {first_heading}" if file_number else first_heading

            # ブックマークを追加
            bookmarks.append((bookmark_title, total_pages))

            # ヘッダーを追加したページを追加
            for page in pdf_reader.pages:
                new_page = add_header(page, bookmark_title)
                writer.add_page(new_page)

            total_pages += len(pdf_reader.pages)
        except Exception as e:
            logger.error(f"Error processing {pdf_file}: {str(e)}")

    # ブックマークを追加
    for title, page_num in bookmarks:
        writer.add_outline_item(title, page_num)

    logger.info("Saving final PDF")
    with open(output_pdf, 'wb') as f:
        writer.write(f)

    logger.info(f"Merged PDF with bookmarks and headers saved as {output_pdf}")

# 使用例
pdf_files = [
    "slide_01.pdf",
    "slide_02.pdf",
    "slide_03.pdf",
    "slide_04.pdf",
    "slide_05.pdf",
    "slide_06.pdf",
    "slide_07.pdf",
    "slide_08.pdf",
    "slide_09.pdf",
    "slide_10.pdf",
    "slide_11.pdf",
    "slide_12.pdf",
    "slide_13.pdf"
]
output_pdf = "merged_with_bookmarks_and_headers.pdf"
merge_pdfs_with_bookmarks_and_header(pdf_files, output_pdf)

終わりに

今回の記事では、Pythonを使用して複数のPDFファイルをマージし、各ページにヘッダーを追加し、ブックマークを設定する方法について詳しく解説しました。PDF操作はビジネスや教育の現場で頻繁に必要とされる作業であり、このプログラムを活用することで、手作業で行っていた煩雑な作業を自動化し、効率化することができます。

このプログラムは、特定のフォントを使用したヘッダーの追加や、ファイル名からの情報抽出など、実用的な機能を備えています。さらに、エラーハンドリングやログ記録を適切に行うことで、信頼性の高いツールとして利用することができます。

この記事が、皆さんのPDF操作の効率化に役立つことを願っています。

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社WEBサイトをご覧ください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.